﻿using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

//Made by Dreadrith#3238
//Version v1.1.0
//Discord: https://discord.gg/ZsPfrGn
//Github: https://github.com/Dreadrith/DreadScripts
//Gumroad: https://gumroad.com/dreadrith
//Ko-fi: https://ko-fi.com/dreadrith

namespace DreadScripts
{
    public class GradientFlood : EditorWindow
    {

        #region Automated Variables
        private static Vector2 scroll;
        private static float modifiedBoundRange;
        private static Texture2D titleTexture;
        #endregion

        #region Input
        public static Texture2D pathTexture;
        public static Color startPixelsColor = Color.red;
        public static Color limitPixelsColor = Color.white;
        public static GradientType gradientType = GradientType.DataGradient;

        public static Color tintColor = Color.white;
        public static Gradient gradientColor = new Gradient();

        public static float gradientDistribution = 1;
        public static float startColorTolerance = 0.05f;
        public static float limitColorTolerance = 0.05f;
        public static float rangeLowerBound;
        public static float rangeUpperBound = 255;
        public static bool invertGradient;
        public static bool loopGradient;
        public static bool applyGradientAlpha;
        #endregion


        public enum GradientType
        {
            DataGradient,
            TintedGradient,
            GradientGradient
        }

        public enum _FloodBehavior
        {
            Side,
            Diagonal,
            SideAndDiagonal,
            Horizontal,
            Vertical,
            Custom
        }

        public static bool foldoutFloodStep;
        public static _FloodBehavior floodBehavior = _FloodBehavior.SideAndDiagonal;
        public static List<FloodStep> floodSteps = new List<FloodStep>()
        {
            new FloodStep
            (
                new Vector2Int(1, 0),
                new Vector2Int(-1, 0),
                new Vector2Int(0, 1),
                new Vector2Int(0, -1),
                new Vector2Int(1, 1),
                new Vector2Int(1, -1),
                new Vector2Int(-1, 1),
                new Vector2Int(-1, -1)

            )
        };



        [MenuItem("Poi/Gradient Flood")]
        private static void showWindow()
        {
            EditorWindow w = GetWindow<GradientFlood>(false, "Gradient Flood", true);
            if (!titleTexture)
            {
                titleTexture = GetColors((Texture2D)EditorGUIUtility.IconContent("Texture2D Icon").image, 16, 16, out _);
                titleTexture.Apply();
            }

            w.titleContent.image = titleTexture;
            w.minSize = new Vector2(423, 253);
        }

        private void OnGUI()
        {
            scroll = EditorGUILayout.BeginScrollView(scroll);
            GUIStyle centeredTitle = new GUIStyle("boldlabel") {alignment = TextAnchor.MiddleCenter, fontSize = 16};
            using (new GUILayout.VerticalScope("helpbox"))
            {
                GUILayout.Label("Texture", centeredTitle);
                using (new GUILayout.HorizontalScope())
                {
                    GUILayout.FlexibleSpace();
                    EditorGUIUtility.labelWidth = 1;
                    pathTexture = (Texture2D) EditorGUILayout.ObjectField(string.Empty, pathTexture, typeof(Texture2D), false, GUILayout.Width(80), GUILayout.Height(80));
                    EditorGUIUtility.labelWidth = 0;

                    GUILayout.FlexibleSpace();
                }

                DrawFloodBehaviorGUI();

                using (new GUILayout.HorizontalScope("box"))
                    gradientType = (GradientType)EditorGUILayout.EnumPopup("Gradient Type", gradientType);


                switch (gradientType)
                {
                    case GradientType.TintedGradient:
                        using (new GUILayout.HorizontalScope("box"))
                            tintColor = EditorGUILayout.ColorField("Tint", tintColor);
                        break;
                    case GradientType.GradientGradient:
                        using (new GUILayout.HorizontalScope("box"))
                            gradientColor = EditorGUILayout.GradientField("Gradient", gradientColor);
                        break;
                }


                if (gradientType > 0)
                {
                    using (new GUILayout.HorizontalScope("box"))
                    {
                        GUILayout.Label("Color Range");

                        EditorGUI.BeginChangeCheck();

                        rangeLowerBound = EditorGUILayout.DelayedIntField((int) rangeLowerBound, GUI.skin.label, GUILayout.Width(28));
                        EditorGUILayout.MinMaxSlider(ref rangeLowerBound, ref rangeUpperBound, 0, 255);

                        rangeUpperBound = EditorGUILayout.DelayedIntField((int) rangeUpperBound, GUI.skin.label, GUILayout.Width(28));

                        if (EditorGUI.EndChangeCheck())
                        {
                            rangeUpperBound = Mathf.Clamp((int) rangeUpperBound, 0, 255);
                            rangeLowerBound = Mathf.Max(0, Mathf.Min(rangeUpperBound, rangeLowerBound));
                        }
                    }
                }
                

                using (new GUILayout.HorizontalScope("box"))
                {
                    using (new GUILayout.HorizontalScope())
                        GUILayout.Label("Tolerance");

                    using (new GUILayout.HorizontalScope())
                    {
                        EditorGUIUtility.labelWidth = 40;
                        startColorTolerance = EditorGUILayout.Slider("Start", startColorTolerance, 0, 1);
                        limitColorTolerance = EditorGUILayout.Slider("Limit", limitColorTolerance, 0, 1);
                        EditorGUIUtility.labelWidth = 0;
                    }
                }

                using (new GUILayout.HorizontalScope("box"))
                {
                    invertGradient = EditorGUILayout.Toggle("Invert Gradient", invertGradient);

                    if (gradientType > 0) applyGradientAlpha = EditorGUILayout.Toggle("Apply Gradient Alpha", applyGradientAlpha);
                }

                using (new GUILayout.HorizontalScope("box"))
                {
                    loopGradient = EditorGUILayout.Toggle("Loop Gradient", loopGradient);
                    if (loopGradient) gradientDistribution = EditorGUILayout.FloatField("Distribution", gradientDistribution);
                }

                using (new EditorGUI.DisabledScope(!pathTexture))
                    if (GUILayout.Button("Fill"))
                        GenerateFilledTexture(pathTexture);
                
            }
            Credit();

            EditorGUILayout.EndScrollView();
        }

        private void DrawFloodBehaviorGUI()
        {
            using (var change = new EditorGUI.ChangeCheckScope())
            {
                using (new GUILayout.HorizontalScope("box"))
                    floodBehavior = (_FloodBehavior)EditorGUILayout.EnumPopup(new GUIContent("Flood Behavior", "The path to fill in the texture starting from the start pixels"), floodBehavior);

                if (change.changed)
                {
                    switch (floodBehavior)
                    {
                        case _FloodBehavior.Side:
                            floodSteps.Clear();
                            floodSteps.Add(new FloodStep
                            (

                                new Vector2Int(1, 0),
                                new Vector2Int(-1, 0),
                                new Vector2Int(0, 1),
                                new Vector2Int(0, -1)

                            ));
                            break;
                        case _FloodBehavior.Diagonal:
                            floodSteps.Clear();
                            floodSteps.Add(new FloodStep
                            (
                                new Vector2Int(1, 1),
                                new Vector2Int(-1, -1),
                                new Vector2Int(1, -1),
                                new Vector2Int(-1, 1)
                            ));
                            break;
                        case _FloodBehavior.SideAndDiagonal:
                            floodSteps.Clear();
                            floodSteps.Add(new FloodStep
                            (

                                new Vector2Int(1, 0),
                                new Vector2Int(-1, 0),
                                new Vector2Int(0, 1),
                                new Vector2Int(0, -1),
                                new Vector2Int(1, 1),
                                new Vector2Int(1, -1),
                                new Vector2Int(-1, 1),
                                new Vector2Int(-1, -1)

                            ));
                            break;
                        case _FloodBehavior.Horizontal:
                            floodSteps.Clear();
                            floodSteps.Add(new FloodStep
                            (
                                new Vector2Int(1, 0),
                                new Vector2Int(-1, 0)
                            ));
                            break;
                        case _FloodBehavior.Vertical:
                            floodSteps.Clear();
                            floodSteps.Add(new FloodStep
                            (
                                new Vector2Int(0, 1),
                                new Vector2Int(0, -1)
                            ));
                            break;
                    }
                }
            }
            if (floodBehavior != _FloodBehavior.Custom) return;

            using (new GUILayout.VerticalScope("box"))
            {
                using (new GUILayout.VerticalScope("helpbox"))
                {
                    foldoutFloodStep = EditorGUILayout.Foldout(foldoutFloodStep, "Flood Steps");
                }

                if (foldoutFloodStep)
                {
                    EditorGUI.indentLevel++;
                    for (int i = 0; i < floodSteps.Count; i++)
                    {
                        DrawFloodStepGUI(floodSteps[i], i);
                    }

                    if (GUILayout.Button("Add Step"))
                        floodSteps.Add(new FloodStep());
                    EditorGUI.indentLevel--;
                }
            }
        }
        private void DrawFloodStepGUI(FloodStep step, int i)
        {
            using (new GUILayout.VerticalScope("box"))
            {
                using (new GUILayout.HorizontalScope("helpbox"))
                {
                    step.foldout = EditorGUILayout.Foldout(step.foldout, $"Step {i}:");

                    using (new EditorGUI.DisabledGroupScope(floodSteps.Count == 1))
                        if (GUILayout.Button("X", GUILayout.Width(20), GUILayout.Height(18)))
                        {
                            floodSteps.RemoveAt(i);
                            goto Skip;
                        }
                }

                if (step.foldout)
                {
                    EditorGUI.indentLevel++;
                    for (int j = 0; j < step.coordinates.Count; j++)
                    {
                        using (new GUILayout.HorizontalScope("box"))
                        {
                            step.coordinates[j] = EditorGUILayout.Vector2IntField(string.Empty, step.coordinates[j]);
                            if (GUILayout.Button("X", GUILayout.Width(20), GUILayout.Height(18)))
                                step.coordinates.RemoveAt(j);
                        }

                        
                    }
                    if (GUILayout.Button("+", "toolbarbutton")) step.coordinates.Add(new Vector2Int());

                    EditorGUI.indentLevel--;
                }
                Skip:;

            }
        }

        public static void GenerateFilledTexture(Texture2D texture)
        {
            if (!loopGradient) gradientDistribution = 1;
            if (gradientType == GradientType.DataGradient)
            {
                rangeLowerBound = 1;
                rangeUpperBound = 255;
            }

            int width = texture.width;
            modifiedBoundRange = rangeUpperBound / 255 - rangeLowerBound / 255;
            Texture2D gradientTexture = GetColors(texture, out Color[] ogColors);
            GradientFillPixel[] gradientPixels = new GradientFillPixel[ogColors.Length];

            Queue<GradientFillPixel> pixelsToExpand = new Queue<GradientFillPixel>();

            for (int i = 0; i < ogColors.Length; i++)
            {
                gradientPixels[i] = new GradientFillPixel(ogColors[i], i);
                if (!gradientPixels[i].isLimit && gradientPixels[i].isFilled) pixelsToExpand.Enqueue(gradientPixels[i]);
            }

            if (pixelsToExpand.Count == 0)
                Debug.LogWarning("<color=red>[GPFiller]</color> No start pixels were found! If start pixels exist, try adjusting the color tolerance.");

            int floodIndex = 0;
            int currentStackCount = pixelsToExpand.Count;
            int nextStackCount = 0;

            bool IsValidFillIndex(int index, out GradientFillPixel pixelToFill)
            {
                if (index >= 0 && index < ogColors.Length)
                {
                    pixelToFill = gradientPixels[index];
                    if (!pixelToFill.isLimit && !pixelToFill.isFilled)
                    {
                        pixelsToExpand.Enqueue(pixelToFill);
                        nextStackCount++;
                        return true;
                    }
                }

                pixelToFill = null;
                return false;
            }

            void FillIndex(GradientFillPixel fillerPixel, int index)
            {
                if (!IsValidFillIndex(index, out GradientFillPixel nextPixel)) return;
                nextPixel.gradientValue = fillerPixel.gradientValue + gradientDistribution;
                nextPixel.isFilled = true;
            }

            

            while (pixelsToExpand.Any())
            {
                FloodStep currentStep = floodSteps[floodIndex % floodSteps.Count];
                var pixel = pixelsToExpand.Dequeue();

                foreach (var coord in currentStep.coordinates)
                {
                    int finalX = (pixel.arrayIndex % width) + coord.x;
                    if (finalX < 0 || finalX >= width) continue;

                    FillIndex(pixel, pixel.arrayIndex + coord.x + width * coord.y);
                }

                currentStackCount--;
                if (currentStackCount == 0)
                {
                    currentStackCount = nextStackCount;
                    nextStackCount = 0;
                    floodIndex++;
                }
            }


            float maxGradientValue = gradientPixels.Max(p => p.gradientValue);
            if (gradientType == GradientType.DataGradient) maxGradientValue /= 4;

            for (int i = 0; i < ogColors.Length; i++)
            {
                var f = gradientPixels[i].GetFloatValue(maxGradientValue);

                if (f == 0) ogColors[i] = Color.clear;
                else
                {

                    if (gradientType == GradientType.TintedGradient)
                        ogColors[i] = new Color(f * tintColor.r, f * tintColor.g, f * tintColor.b, gradientPixels[i].isLimit ? 1 : (applyGradientAlpha ? f : 1) * tintColor.a);
                    else if (gradientType == GradientType.GradientGradient)
                    {
                        Color gradColor = gradientColor.Evaluate(f);
                        gradColor.a = gradientPixels[i].isLimit ? 1 : (applyGradientAlpha ? f : 1) * gradColor.a;
                        ogColors[i] = gradColor;
                    }
                    else
                    {
                        if (f <= 1) ogColors[i] = new Color(f % 1, 0, 0, 0);

                        else if (f <= 2) ogColors[i] = new Color(1, f%1, 0, 0);

                            else if (f <= 3) ogColors[i] = new Color(1, 1, f % 1, 0);

                        else ogColors[i] = new Color(1, 1, 1, f % 1);
                        
                    }
                }
            }

            gradientTexture.SetPixels(ogColors);
            gradientTexture.Apply();

            string assetPath = AssetDatabase.GetAssetPath(pathTexture);
            string ext = Path.GetExtension(assetPath);
            byte[] data;
            switch (ext)
            {
                case ".jpeg" when !applyGradientAlpha && gradientType != GradientType.DataGradient:
                case ".jpg" when !applyGradientAlpha && gradientType != GradientType.DataGradient:
                    ext = ".jpg";
                    data = gradientTexture.EncodeToJPG(100);
                    break;
                case ".tga":
                    data = gradientTexture.EncodeToTGA();
                    break;
                default:
                    ext = ".png";
                    data = gradientTexture.EncodeToPNG();
                    break;
            }


            DestroyImmediate(gradientTexture);

            string savePath = AssetDatabase.GenerateUniqueAssetPath($"{Path.GetDirectoryName(assetPath)}/{pathTexture.name}{ext}");
            SaveTexture(data, savePath);
            CopyTextureSettings(assetPath, savePath);
        }

        public static Texture2D GetColors(Texture2D texture, int width, int height, out Color[] Colors, bool unloadTempTexture = false)
        {
            //Thanks to
            //https://gamedev.stackexchange.com/questions/92285/unity3d-resize-texture-without-corruption
            texture.filterMode = FilterMode.Point;
            RenderTexture rt = RenderTexture.GetTemporary(width, height);

            rt.filterMode = FilterMode.Point;
            RenderTexture.active = rt;
            Graphics.Blit(texture, rt);
            Texture2D newTexture = new Texture2D(width, height);
            newTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
            Color[] myColors = newTexture.GetPixels();
            RenderTexture.active = null;
            /////////////////////
            Colors = myColors;
            if (unloadTempTexture)
            {
                DestroyImmediate(newTexture);
                return null;
            }
            return newTexture;
        }

        public static Texture2D GetColors(Texture2D texture, out Color[] Colors, bool unloadTempTexture = false)
        {
            return GetColors(texture, texture.width, texture.height, out Colors, unloadTempTexture);
        }

        private static void CopyTextureSettings(string from, string to)
        {
            TextureImporter source = (TextureImporter)AssetImporter.GetAtPath(from);
            TextureImporterSettings sourceSettings = new TextureImporterSettings();
            source.ReadTextureSettings(sourceSettings);

            TextureImporter destination = (TextureImporter)AssetImporter.GetAtPath(to);
            destination.SetTextureSettings(sourceSettings);
            destination.maxTextureSize = source.maxTextureSize;
            destination.textureCompression = source.textureCompression;
            destination.crunchedCompression = source.crunchedCompression;
            destination.SaveAndReimport();
        }

        private static void SaveTexture(byte[] textureEncoding, string path)
        {
            using (System.IO.FileStream stream = System.IO.File.Create(path))
                stream.Write(textureEncoding, 0, textureEncoding.Length);
            AssetDatabase.Refresh();
            EditorGUIUtility.PingObject(AssetDatabase.LoadMainAssetAtPath(path));
        }

        public class GradientFillPixel
        {
            public readonly bool isLimit;
            public bool isFilled;
            public float gradientValue;
            public int arrayIndex;

            public GradientFillPixel(Color pixelColor, int index)
            {
                isLimit = Mathf.Abs(pixelColor.r - limitPixelsColor.r) < limitColorTolerance &&
                          Mathf.Abs(pixelColor.g - limitPixelsColor.g) < limitColorTolerance &&
                          Mathf.Abs(pixelColor.b - limitPixelsColor.b) < limitColorTolerance;

                isFilled = Mathf.Abs(pixelColor.r - startPixelsColor.r) < startColorTolerance &&
                           Mathf.Abs(pixelColor.g - startPixelsColor.g) < startColorTolerance &&
                           Mathf.Abs(pixelColor.b - startPixelsColor.b) < startColorTolerance;

                gradientValue = (int)rangeLowerBound;
                arrayIndex = index;
            }

            public float GetFloatValue(float maxGradientValue)
            {
                if (isLimit || !isFilled) return 0;
                float floatValue = !loopGradient ? gradientValue / maxGradientValue : (gradientValue % 255 * (gradientType == GradientType.DataGradient ? 4 : 1)) / 255f;
                float adjustedValue = floatValue * modifiedBoundRange + rangeLowerBound / 255;
                return invertGradient ? (gradientType == GradientType.DataGradient ? 4 : 1) - adjustedValue : adjustedValue;
            }
        }

        public class FloodStep
        {
            public List<Vector2Int> coordinates;
            public bool foldout;

            public FloodStep(params Vector2Int[] coordinates)
            {
                this.coordinates = coordinates.ToList();
            }
        }


        private static void Credit()
        {
            using (new GUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                if (GUILayout.Button("Made By Dreadrith#3238", "boldlabel"))
                    Application.OpenURL("https://linktr.ee/Dreadrith");
            }
        }
    }


}
